package org.javaqq.face.main.base;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.IllegalComponentStateException;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.FocusListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Vector;

import javax.accessibility.Accessible;
import javax.accessibility.AccessibleComponent;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleIcon;
import javax.accessibility.AccessibleRole;
import javax.accessibility.AccessibleState;
import javax.accessibility.AccessibleStateSet;
import javax.accessibility.AccessibleTable;
import javax.accessibility.AccessibleText;
import javax.swing.JEditorPane;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import javax.swing.text.Position;
import javax.swing.text.Segment;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.View;
import javax.swing.text.html.HTML;
import javax.swing.text.html.ImageView;

public class AccessibleHTML implements Accessible {

    /**
     * The editor.
     */
    private JEditorPane editor;
    /**
     * Current model.
     */
    private Document model;
    /**
     * DocumentListener installed on the current model.
     */
    private DocumentListener docListener;
    /**
     * PropertyChangeListener installed on the editor
     */
    private PropertyChangeListener propChangeListener;
    /**
     * The root ElementInfo for the document
     */
    private ElementInfo rootElementInfo;
    /*
     * The root accessible context for the document
     */
    private RootHTMLAccessibleContext rootHTMLAccessibleContext;

    public AccessibleHTML(JEditorPane pane) {
	editor = pane;
	propChangeListener = new PropertyChangeHandler();
        setDocument(editor.getDocument());

        docListener = new DocumentHandler();
    }

    /**
     * Sets the document.
     */
    private void setDocument(Document document) {
        if (model != null) {
            model.removeDocumentListener(docListener);
        }
	if (editor != null) {
	    editor.removePropertyChangeListener(propChangeListener);
	}
        this.model = document;
        if (model != null) {
            if (rootElementInfo != null) {
                rootElementInfo.invalidate(false);
            }
            buildInfo();
            model.addDocumentListener(docListener);
        }
        else {
            rootElementInfo = null;
        }
	if (editor != null) {
	    editor.addPropertyChangeListener(propChangeListener);
	}
    }

    /**
     * Returns the Document currently presenting information for.
     */
    private Document getDocument() {
        return model;
    }

    /**
     * Returns the JEditorPane providing information for.
     */
    private JEditorPane getTextComponent() {
        return editor;
    }

    /**
     * Returns the ElementInfo representing the root Element.
     */
    private ElementInfo getRootInfo() {
        return rootElementInfo;
    }

    /**
     * Returns the root <code>View</code> associated with the current text
     * component.
     */
    private View getRootView() {
        return getTextComponent().getUI().getRootView(getTextComponent());
    }

    /**
     * Returns the bounds the root View will be rendered in.
     */
    private Rectangle getRootEditorRect() {
        Rectangle alloc = getTextComponent().getBounds();
        if ((alloc.width > 0) && (alloc.height > 0)) {
            alloc.x = alloc.y = 0;
            Insets insets = editor.getInsets();
            alloc.x += insets.left;
            alloc.y += insets.top;
            alloc.width -= insets.left + insets.right;
            alloc.height -= insets.top + insets.bottom;
            return alloc;
        }
        return null;
    }

    /**
     * If possible acquires a lock on the Document.  If a lock has been
     * obtained a key will be retured that should be passed to
     * <code>unlock</code>.
     */
    private Object lock() {
        Document document = getDocument();

        if (document instanceof AbstractDocument) {
            ((AbstractDocument)document).readLock();
            return document;
        }
        return null;
    }

    /**
     * Releases a lock previously obtained via <code>lock</code>.
     */
    private void unlock(Object key) {
        if (key != null) {
            ((AbstractDocument)key).readUnlock();
        }
    }

    /**
     * Rebuilds the information from the current info.
     */
    private void buildInfo() {
        Object lock = lock();

        try {
            Document doc = getDocument();
            Element root = doc.getDefaultRootElement();

            rootElementInfo = new ElementInfo(root);
            rootElementInfo.validate();
        } finally {
            unlock(lock);
        }
    }

    /*
     * Create an ElementInfo subclass based on the passed in Element.
     */
    ElementInfo createElementInfo(Element e, ElementInfo parent) {
        AttributeSet attrs = e.getAttributes();

        if (attrs != null) {
            Object name = attrs.getAttribute(StyleConstants.NameAttribute);

            if (name == HTML.Tag.IMG) {
                return new IconElementInfo(e, parent);
            }
            else if (name == HTML.Tag.CONTENT || name == HTML.Tag.CAPTION) {
                return new TextElementInfo(e, parent);
            }
            else if (name == HTML.Tag.TABLE) {
                return new TableElementInfo(e, parent);
            }
        }
        return null;
    }
    
    /**
     * Returns the root AccessibleContext for the document
     */
    public AccessibleContext getAccessibleContext() {
	if (rootHTMLAccessibleContext == null) {
	    rootHTMLAccessibleContext = 
		new RootHTMLAccessibleContext(rootElementInfo);
	}
	return rootHTMLAccessibleContext;
    }
    
    /*
     * The roow AccessibleContext for the document
     */
    private class RootHTMLAccessibleContext extends HTMLAccessibleContext {

	public RootHTMLAccessibleContext(ElementInfo elementInfo) {
	    super(elementInfo);
	}

	/**
	 * Gets the accessibleName property of this object.  The accessibleName
	 * property of an object is a localized String that designates the purpose
	 * of the object.  For example, the accessibleName property of a label
	 * or button might be the text of the label or button itself.  In the
	 * case of an object that doesn't display its name, the accessibleName
	 * should still be set.  For example, in the case of a text field used
	 * to enter the name of a city, the accessibleName for the en_US locale
	 * could be 'city.'
	 *
	 * @return the localized name of the object; null if this 
	 * object does not have a name
	 *
	 * @see #setAccessibleName
	 */
	public String getAccessibleName() {
	    if (model != null) {
		return (String)model.getProperty(Document.TitleProperty);
	    } else {
		return null;
	    }
	}
	
	/**
	 * Gets the accessibleDescription property of this object.  If this
	 * property isn't set, returns the content type of this
	 * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text").
	 *
	 * @return the localized description of the object; <code>null</code>
	 * 	if this object does not have a description
	 *
	 * @see #setAccessibleName
	 */
	public String getAccessibleDescription() {
	    return editor.getContentType();
	}
	
	/**
	 * Gets the role of this object.  The role of the object is the generic
	 * purpose or use of the class of this object.  For example, the role
	 * of a push button is AccessibleRole.PUSH_BUTTON.  The roles in 
	 * AccessibleRole are provided so component developers can pick from
	 * a set of predefined roles.  This enables assistive technologies to
	 * provide a consistent interface to various tweaked subclasses of 
	 * components (e.g., use AccessibleRole.PUSH_BUTTON for all components
	 * that act like a push button) as well as distinguish between sublasses
	 * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes
	 * and AccessibleRole.RADIO_BUTTON for radio buttons).
	 * <p>Note that the AccessibleRole class is also extensible, so 
	 * custom component developers can define their own AccessibleRole's
	 * if the set of predefined roles is inadequate.
	 *
	 * @return an instance of AccessibleRole describing the role of the object
	 * @see AccessibleRole
	 */
	public AccessibleRole getAccessibleRole() {
	    return AccessibleRole.TEXT;
	}
    }

    /*
     * Base AccessibleContext class for HTML elements
     */
    protected abstract class HTMLAccessibleContext extends AccessibleContext
        implements Accessible, AccessibleComponent {

	protected ElementInfo elementInfo;

	public HTMLAccessibleContext(ElementInfo elementInfo) {
	    this.elementInfo = elementInfo;
	}

	// begin AccessibleContext implementation ...
	public AccessibleContext getAccessibleContext() {
	    return this;
	}

	/**
	 * Gets the state set of this object.
	 *
	 * @return an instance of AccessibleStateSet describing the states
	 * of the object
	 * @see AccessibleStateSet
	 */
	public AccessibleStateSet getAccessibleStateSet() {
	    AccessibleStateSet states = new AccessibleStateSet();
	    Component comp = getTextComponent();

	    if (comp.isEnabled()) {
		states.add(AccessibleState.ENABLED);
	    }
	    if (comp instanceof JTextComponent &&
		((JTextComponent)comp).isEditable()) {
		
		states.add(AccessibleState.EDITABLE);
		states.add(AccessibleState.FOCUSABLE);
	    }
	    if (comp.isVisible()) {
		states.add(AccessibleState.VISIBLE);
	    }
	    if (comp.isShowing()) {
		states.add(AccessibleState.SHOWING);
	    }
	    return states;
	}
	
	/**
	 * Gets the 0-based index of this object in its accessible parent.
	 *
	 * @return the 0-based index of this object in its parent; -1 if this 
	 * object does not have an accessible parent.
	 *
	 * @see #getAccessibleParent 
	 * @see #getAccessibleChildrenCount
	 * @see #getAccessibleChild
	 */
	public int getAccessibleIndexInParent() {
	    return elementInfo.getIndexInParent();
	}
	
	/**
	 * Returns the number of accessible children of the object.
	 *
	 * @return the number of accessible children of the object.
	 */
	public int getAccessibleChildrenCount() {
	    return elementInfo.getChildCount();
	}
	
	/**
	 * Returns the specified Accessible child of the object.  The Accessible
	 * children of an Accessible object are zero-based, so the first child 
	 * of an Accessible child is at index 0, the second child is at index 1,
	 * and so on.
	 *
	 * @param i zero-based index of child
	 * @return the Accessible child of the object
	 * @see #getAccessibleChildrenCount
	 */
	public Accessible getAccessibleChild(int i) {
	    ElementInfo childInfo = elementInfo.getChild(i);
	    if (childInfo != null && childInfo instanceof Accessible) {
		return (Accessible)childInfo;
	    } else {
		return null;
	    }
	}
	
	/** 
	 * Gets the locale of the component. If the component does not have a 
	 * locale, then the locale of its parent is returned.  
	 *
	 * @return this component's locale.  If this component does not have 
	 * a locale, the locale of its parent is returned.
	 *
	 * @exception IllegalComponentStateException 
	 * If the Component does not have its own locale and has not yet been 
	 * added to a containment hierarchy such that the locale can be
	 * determined from the containing parent. 
	 */
	public Locale getLocale() throws IllegalComponentStateException {
	    return editor.getLocale();
	}
	// ... end AccessibleContext implementation

	// begin AccessibleComponent implementation ...
	public AccessibleComponent getAccessibleComponent() {
	    return this;
	}
	
	/**
	 * Gets the background color of this object.
	 *
	 * @return the background color, if supported, of the object; 
	 * otherwise, null
	 * @see #setBackground
	 */
	public Color getBackground() {
	    return getTextComponent().getBackground();
	}
	
	/**
	 * Sets the background color of this object.
	 *
	 * @param c the new Color for the background
	 * @see #setBackground
	 */
	public void setBackground(Color c) {
	    getTextComponent().setBackground(c);
	}
	
	/**
	 * Gets the foreground color of this object.
	 *
	 * @return the foreground color, if supported, of the object; 
	 * otherwise, null
	 * @see #setForeground
	 */
	public Color getForeground() {
	    return getTextComponent().getForeground();
	}
	
	/**
	 * Sets the foreground color of this object.
	 *
	 * @param c the new Color for the foreground
	 * @see #getForeground
	 */
	public void setForeground(Color c) {
	    getTextComponent().setForeground(c);
	}
	
	/**
	 * Gets the Cursor of this object.
	 *
	 * @return the Cursor, if supported, of the object; otherwise, null
	 * @see #setCursor
	 */
	public Cursor getCursor() {
	    return getTextComponent().getCursor();
	}
	
	/**
	 * Sets the Cursor of this object.
	 *
	 * @param c the new Cursor for the object
	 * @see #getCursor
	 */
	public void setCursor(Cursor cursor) {
	    getTextComponent().setCursor(cursor);
	}
	
	/**
	 * Gets the Font of this object.
	 *
	 * @return the Font,if supported, for the object; otherwise, null
	 * @see #setFont
	 */
	public Font getFont() {
	    return getTextComponent().getFont();
	}
	
	/**
	 * Sets the Font of this object.
	 *
	 * @param f the new Font for the object
	 * @see #getFont
	 */
	public void setFont(Font f) {
	    getTextComponent().setFont(f);
	}
	
	/**
	 * Gets the FontMetrics of this object.
	 *
	 * @param f the Font
	 * @return the FontMetrics, if supported, the object; otherwise, null
	 * @see #getFont
	 */
	public FontMetrics getFontMetrics(Font f) {
	    return getTextComponent().getFontMetrics(f);
	}
	
	/**
	 * Determines if the object is enabled.  Objects that are enabled
	 * will also have the AccessibleState.ENABLED state set in their
	 * AccessibleStateSets.
	 *
	 * @return true if object is enabled; otherwise, false
	 * @see #setEnabled
	 * @see AccessibleContext#getAccessibleStateSet
	 * @see AccessibleState#ENABLED
	 * @see AccessibleStateSet
	 */
	public boolean isEnabled() {
	    return getTextComponent().isEnabled();
	}
	
	/**
	 * Sets the enabled state of the object.
	 *
	 * @param b if true, enables this object; otherwise, disables it 
	 * @see #isEnabled
	 */
	public void setEnabled(boolean b) {
	    getTextComponent().setEnabled(b);
	}
	
	/**
	 * Determines if the object is visible.  Note: this means that the
	 * object intends to be visible; however, it may not be
	 * showing on the screen because one of the objects that this object
	 * is contained by is currently not visible.  To determine if an object
	 * is showing on the screen, use isShowing().
	 * <p>Objects that are visible will also have the 
	 * AccessibleState.VISIBLE state set in their AccessibleStateSets.
	 *
	 * @return true if object is visible; otherwise, false
	 * @see #setVisible
	 * @see AccessibleContext#getAccessibleStateSet
	 * @see AccessibleState#VISIBLE
	 * @see AccessibleStateSet
	 */
	public boolean isVisible() {
	    return getTextComponent().isVisible();
	}
	
	/**
	 * Sets the visible state of the object.
	 *
	 * @param b if true, shows this object; otherwise, hides it 
	 * @see #isVisible
	 */
	public void setVisible(boolean b) {
	    getTextComponent().setVisible(b);
	}
	
	/**
	 * Determines if the object is showing.  This is determined by checking
	 * the visibility of the object and its ancestors.
	 * Note: this
	 * will return true even if the object is obscured by another (for 
	 * example, it is underneath a menu that was pulled down).
	 *
	 * @return true if object is showing; otherwise, false
	 */
	public boolean isShowing() {
	    return getTextComponent().isShowing();
	}
	
	/** 
	 * Checks whether the specified point is within this object's bounds,
	 * where the point's x and y coordinates are defined to be relative 
	 * to the coordinate system of the object. 
	 *
	 * @param p the Point relative to the coordinate system of the object
	 * @return true if object contains Point; otherwise false
	 * @see #getBounds
	 */
	public boolean contains(Point p) {
	    Rectangle r = getBounds();
	    if (r != null) {
		return r.contains(p.x, p.y);
	    } else {
		return false;
	    }
	}
	
	/** 
	 * Returns the location of the object on the screen.
	 *
	 * @return the location of the object on screen; null if this object
	 * is not on the screen
	 * @see #getBounds
	 * @see #getLocation
	 */
	public Point getLocationOnScreen() {
	    Point editorLocation = getTextComponent().getLocationOnScreen();
	    Rectangle r = getBounds();
	    if (r != null) {
		return new Point(editorLocation.x + r.x,
				 editorLocation.y + r.y);
	    } else {
		return null;
	    }
	}
	
	/** 
	 * Gets the location of the object relative to the parent in the form 
	 * of a point specifying the object's top-left corner in the screen's 
	 * coordinate space.
	 *
	 * @return An instance of Point representing the top-left corner of the 
	 * object's bounds in the coordinate space of the screen; null if
	 * this object or its parent are not on the screen
	 * @see #getBounds
	 * @see #getLocationOnScreen
	 */
	public Point getLocation() { 
	    Rectangle r = getBounds();
	    if (r != null) {
		return new Point(r.x, r.y);
	    } else {
		return null;
	    }
	}
	
	/** 
	 * Sets the location of the object relative to the parent.
	 * @param p the new position for the top-left corner
	 * @see #getLocation
	 */
	public void setLocation(Point p) {
	}
	
	/** 
	 * Gets the bounds of this object in the form of a Rectangle object. 
	 * The bounds specify this object's width, height, and location
	 * relative to its parent. 
	 *
	 * @return A rectangle indicating this component's bounds; null if 
	 * this object is not on the screen.
	 * @see #contains
	 */
	public Rectangle getBounds() {
	    return elementInfo.getBounds();
	}
	
	/** 
	 * Sets the bounds of this object in the form of a Rectangle object. 
	 * The bounds specify this object's width, height, and location
	 * relative to its parent.
	 *	
	 * @param r rectangle indicating this component's bounds
	 * @see #getBounds
	 */
	public void setBounds(Rectangle r) {
	}
	
	/** 
	 * Returns the size of this object in the form of a Dimension object. 
	 * The height field of the Dimension object contains this object's
	 * height, and the width field of the Dimension object contains this 
	 * object's width. 
	 *
	 * @return A Dimension object that indicates the size of this component; 
	 * null if this object is not on the screen
	 * @see #setSize
	 */
	public Dimension getSize() {
	    Rectangle r = getBounds();
	    if (r != null) {
		return new Dimension(r.width, r.height);
	    } else {
		return null;
	    }
	}
	
	/** 
	 * Resizes this object so that it has width and height. 
	 *	
	 * @param d The dimension specifying the new size of the object. 
	 * @see #getSize
	 */
	public void setSize(Dimension d) {
	    Component comp = getTextComponent();
	    comp.setSize(d);
	}
	
	/**
	 * Returns the Accessible child, if one exists, contained at the local 
	 * coordinate Point.
	 *
	 * @param p The point relative to the coordinate system of this object.
	 * @return the Accessible, if it exists, at the specified location; 
	 * otherwise null
	 */
	public Accessible getAccessibleAt(Point p) {
	    ElementInfo innerMostElement = getElementInfoAt(rootElementInfo, p);
	    if (innerMostElement instanceof Accessible) {
		return (Accessible)innerMostElement;
	    } else {
		return null;
	    }
	}

        private ElementInfo getElementInfoAt(ElementInfo elementInfo, Point p) {
	    if (elementInfo.getBounds() == null) {
		return null;
	    }
	    if (elementInfo.getChildCount() == 0 &&
		elementInfo.getBounds().contains(p)) {
		return elementInfo;
		
	    } else {
		if (elementInfo instanceof TableElementInfo) {
		    // Handle table caption as a special case since it's the
		    // only table child that is not a table row.
		    ElementInfo captionInfo = 
			((TableElementInfo)elementInfo).getCaptionInfo();
		    if (captionInfo != null) {
			Rectangle bounds = captionInfo.getBounds();
			if (bounds != null && bounds.contains(p)) {
			    return captionInfo;
			}
		    }
		}
		for (int i = 0; i < elementInfo.getChildCount(); i++)
{
		    ElementInfo childInfo = elementInfo.getChild(i);
                    ElementInfo retValue = getElementInfoAt(childInfo, p);
                    if (retValue != null) {
                        return retValue;
                    }
		}
	    }
            return null;
	}
	
	/**
	 * Returns whether this object can accept focus or not.   Objects that 
	 * can accept focus will also have the AccessibleState.FOCUSABLE state 
	 * set in their AccessibleStateSets.
	 *
	 * @return true if object can accept focus; otherwise false
	 * @see AccessibleContext#getAccessibleStateSet
	 * @see AccessibleState#FOCUSABLE
	 * @see AccessibleState#FOCUSED
	 * @see AccessibleStateSet
	 */
	public boolean isFocusTraversable() {
	    Component comp = getTextComponent();
	    if (comp instanceof JTextComponent) {
		if (((JTextComponent)comp).isEditable()) {
		    return true;
		}
	    }
	    return false;
	}
	
	/**
	 * Requests focus for this object.  If this object cannot accept focus,
	 * nothing will happen.  Otherwise, the object will attempt to take
	 * focus.
	 * @see #isFocusTraversable
	 */
	public void requestFocus() {
            // TIGER - 4856191
            if (! isFocusTraversable()) {
                return;
            }

	    Component comp = getTextComponent();
	    if (comp instanceof JTextComponent) {

		comp.requestFocusInWindow();

		try {
		    if (elementInfo.validateIfNecessary()) {
			// set the caret position to the start of this component
			Element elem = elementInfo.getElement();
			((JTextComponent)comp).setCaretPosition(elem.getStartOffset());
			
			// fire a AccessibleState.FOCUSED property change event
			AccessibleContext ac = editor.getAccessibleContext();
			PropertyChangeEvent pce = new PropertyChangeEvent(this,
			    AccessibleContext.ACCESSIBLE_STATE_PROPERTY,
			    null, AccessibleState.FOCUSED);
			ac.firePropertyChange(
			    AccessibleContext.ACCESSIBLE_STATE_PROPERTY,
			    null, pce);
		    }
		} catch (IllegalArgumentException e) {
		    // don't fire property change event
		}
	    }
	}
	
	/**
	 * Adds the specified focus listener to receive focus events from this 
	 * component. 
	 *
	 * @param l the focus listener
	 * @see #removeFocusListener
	 */
	public void addFocusListener(FocusListener l) {
	    getTextComponent().addFocusListener(l);
	}
	
	/**
	 * Removes the specified focus listener so it no longer receives focus 
	 * events from this component.
	 *
	 * @param l the focus listener
	 * @see #addFocusListener
	 */
	public void removeFocusListener(FocusListener l) {
	    getTextComponent().removeFocusListener(l);
	}
	// ... end AccessibleComponent implementation
    } // ... end HTMLAccessibleContext


 
    /*
     * ElementInfo for text
     */
    class TextElementInfo extends ElementInfo implements Accessible {

        TextElementInfo(Element element, ElementInfo parent) {
            super(element, parent);
        }

	// begin AccessibleText implementation ...
	private AccessibleContext accessibleContext;
	
	public AccessibleContext getAccessibleContext() {
	    if (accessibleContext == null) {
		accessibleContext = new TextAccessibleContext(this);
	    }
	    return accessibleContext;
	}
	
	/*
	 * AccessibleContext for text elements
	 */
	public class TextAccessibleContext extends HTMLAccessibleContext
            implements AccessibleText {

	    public TextAccessibleContext(ElementInfo elementInfo) {
		super(elementInfo);
	    }
	    
	    public AccessibleText getAccessibleText() {
		return this;
	    }
	    
	    /**
	     * Gets the accessibleName property of this object.  The accessibleName
	     * property of an object is a localized String that designates the purpose
	     * of the object.  For example, the accessibleName property of a label
	     * or button might be the text of the label or button itself.  In the
	     * case of an object that doesn't display its name, the accessibleName
	     * should still be set.  For example, in the case of a text field used
	     * to enter the name of a city, the accessibleName for the en_US locale
	     * could be 'city.'
	     *
	     * @return the localized name of the object; null if this 
	     * object does not have a name
	     *
	     * @see #setAccessibleName
	     */
	    public String getAccessibleName() {
		if (model != null) {
		    return (String)model.getProperty(Document.TitleProperty);
		} else {
		    return null;
		}
	    }
	    
	    /**
	     * Gets the accessibleDescription property of this object.  If this
	     * property isn't set, returns the content type of this
	     * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text").
	     *
	     * @return the localized description of the object; <code>null</code>
	     * 	if this object does not have a description
	     *
	     * @see #setAccessibleName
	     */
	    public String getAccessibleDescription() {
		return editor.getContentType();
	    }
	    
	    /**
	     * Gets the role of this object.  The role of the object is the generic
	     * purpose or use of the class of this object.  For example, the role
	     * of a push button is AccessibleRole.PUSH_BUTTON.  The roles in 
	     * AccessibleRole are provided so component developers can pick from
	     * a set of predefined roles.  This enables assistive technologies to
	     * provide a consistent interface to various tweaked subclasses of 
	     * components (e.g., use AccessibleRole.PUSH_BUTTON for all components
	     * that act like a push button) as well as distinguish between sublasses
	     * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes
	     * and AccessibleRole.RADIO_BUTTON for radio buttons).
	     * <p>Note that the AccessibleRole class is also extensible, so 
	     * custom component developers can define their own AccessibleRole's
	     * if the set of predefined roles is inadequate.
	     *
	     * @return an instance of AccessibleRole describing the role of the object
	     * @see AccessibleRole
	     */
	    public AccessibleRole getAccessibleRole() {
		return AccessibleRole.TEXT;
	    }
	    
	    /**
	     * Given a point in local coordinates, return the zero-based index
	     * of the character under that Point.  If the point is invalid,
	     * this method returns -1.
	     *
	     * @param p the Point in local coordinates
	     * @return the zero-based index of the character under Point p; if 
	     * Point is invalid returns -1.
	     */
	    public int getIndexAtPoint(Point p) {
		View v = getView();
		if (v != null) {
		    return v.viewToModel(p.x, p.y, getBounds());
		} else {
		    return -1;
		}
	    }
	    
	    /**
	     * Determine the bounding box of the character at the given 
	     * index into the string.  The bounds are returned in local
	     * coordinates.  If the index is invalid an empty rectangle is 
	     * returned.
	     *
	     * @param i the index into the String
	     * @return the screen coordinates of the character's the bounding box,
	     * if index is invalid returns an empty rectangle.
	     */
	    public Rectangle getCharacterBounds(int i) {
		try {
		    return editor.getUI().modelToView(editor, i);
		} catch (BadLocationException e) {
		    return null;
		}
	    }
	    
	    /**
	     * Return the number of characters (valid indicies) 
	     *
	     * @return the number of characters
	     */
	    public int getCharCount() {
		if (validateIfNecessary()) {
		    Element elem = elementInfo.getElement();
		    return elem.getEndOffset() - elem.getStartOffset();
		}
		return 0;
	    }
	    
	    /**
	     * Return the zero-based offset of the caret.
	     *
	     * Note: That to the right of the caret will have the same index
	     * value as the offset (the caret is between two characters).
	     * @return the zero-based offset of the caret.
	     */
	    public int getCaretPosition() {
		View v = getView();
		if (v == null) {
		    return -1;
		}
		Container c = v.getContainer();
		if (c == null) {
		    return -1;
		}
		if (c instanceof JTextComponent) {
		    return ((JTextComponent)c).getCaretPosition();
		} else {
		    return -1;
		}
	    }
	    
	    /**
	     * IndexedSegment extends Segment adding the offset into the
	     * the model the <code>Segment</code> was asked for.
	     */
	    private class IndexedSegment extends Segment {
		/**
		 * Offset into the model that the position represents.
		 */
		public int modelOffset;
	    }
	    
	    public String getAtIndex(int part, int index) {
		return getAtIndex(part, index, 0);
	    }
	    
	    
	    public String getAfterIndex(int part, int index) {
		return getAtIndex(part, index, 1);
	    }
	    
	    public String getBeforeIndex(int part, int index) {
		return getAtIndex(part, index, -1);
	    }
	    
	    /**
	     * Gets the word, sentence, or character at <code>index</code>.
	     * If <code>direction</code> is non-null this will find the
	     * next/previous word/sentence/character.
	     */
	    private String getAtIndex(int part, int index, int direction) {
		if (model instanceof AbstractDocument) {
		    ((AbstractDocument)model).readLock();
		}
		try {
		    if (index < 0 || index >= model.getLength()) {
			return null;
		    }
		    switch (part) {
		    case AccessibleText.CHARACTER:
			if (index + direction < model.getLength() &&
			    index + direction >= 0) {
			    return model.getText(index + direction, 1);
			}
			break;
			
			
		    case AccessibleText.WORD:
		    case AccessibleText.SENTENCE:
			IndexedSegment seg = getSegmentAt(part, index);
			if (seg != null) {
			    if (direction != 0) {
				int next;
				
				
				if (direction < 0) {
				    next = seg.modelOffset - 1;
				}
				else {
				    next = seg.modelOffset + direction * seg.count;
				}
				if (next >= 0 && next <= model.getLength()) {
				    seg = getSegmentAt(part, next);
				}
				else {
				    seg = null;
				}
			    }
			    if (seg != null) {
				return new String(seg.array, seg.offset,
                                                  seg.count);
			    }
			}
			break;
			
		    default:
			break;
		    }
		} catch (BadLocationException e) {
		} finally {
		    if (model instanceof AbstractDocument) {
			((AbstractDocument)model).readUnlock();
		    }
		}
		return null;
	    }
	    
	    /*
	     * Returns the paragraph element for the specified index.
	     */
	    private Element getParagraphElement(int index) {
		if (model instanceof PlainDocument ) {
		    PlainDocument sdoc = (PlainDocument)model;
		    return sdoc.getParagraphElement(index);
		} else if (model instanceof StyledDocument) {
		    StyledDocument sdoc = (StyledDocument)model;
		    return sdoc.getParagraphElement(index);
		} else {
		    Element para = null;
		    for (para = model.getDefaultRootElement(); ! para.isLeaf(); ) {
			int pos = para.getElementIndex(index);
			para = para.getElement(pos);
		    }
		    if (para == null) {
			return null;
		    }
		    return para.getParentElement();
		}
	    }
	    
	    /*
	     * Returns a <code>Segment</code> containing the paragraph text
	     * at <code>index</code>, or null if <code>index</code> isn't
	     * valid.
	     */
	    private IndexedSegment getParagraphElementText(int index)
		throws BadLocationException {
		Element para = getParagraphElement(index);
		
		
		if (para != null) {
		    IndexedSegment segment = new IndexedSegment();
		    try {
			int length = para.getEndOffset() - para.getStartOffset();
			model.getText(para.getStartOffset(), length, segment);
		    } catch (BadLocationException e) {
			return null;
		    }
		    segment.modelOffset = para.getStartOffset();
		    return segment;
		}
		return null;
	    }
	    
	    
	    /**
	     * Returns the Segment at <code>index</code> representing either
	     * the paragraph or sentence as identified by <code>part</code>, or
	     * null if a valid paragraph/sentence can't be found. The offset
	     * will point to the start of the word/sentence in the array, and
	     * the modelOffset will point to the location of the word/sentence
	     * in the model.
	     */
	    private IndexedSegment getSegmentAt(int part, int index) 
                throws BadLocationException {

		IndexedSegment seg = getParagraphElementText(index);
		if (seg == null) {
		    return null;
		}
		BreakIterator iterator;
		switch (part) {
		case AccessibleText.WORD:
		    iterator = BreakIterator.getWordInstance(getLocale());
		    break;
		case AccessibleText.SENTENCE:
		    iterator = BreakIterator.getSentenceInstance(getLocale());
		    break;
		default:
		    return null;
		}
		seg.first();
		iterator.setText(seg);
		int end = iterator.following(index - seg.modelOffset + seg.offset);
		if (end == BreakIterator.DONE) {
		    return null;
		}
		if (end > seg.offset + seg.count) {
		    return null;
		}
		int begin = iterator.previous();
		if (begin == BreakIterator.DONE ||
		    begin >= seg.offset + seg.count) {
		    return null;
		}
		seg.modelOffset = seg.modelOffset + begin - seg.offset;
		seg.offset = begin;
		seg.count = end - begin;
		return seg;
	    }
	    
	    /**
	     * Return the AttributeSet for a given character at a given index
	     *
	     * @param i the zero-based index into the text 
	     * @return the AttributeSet of the character
	     */
	    public AttributeSet getCharacterAttribute(int i) {
		if (model instanceof StyledDocument) {
		    StyledDocument doc = (StyledDocument)model;
		    Element elem = doc.getCharacterElement(i);
		    if (elem != null) {
			return elem.getAttributes();
		    }
		}
		return null;
	    }
	    
	    /**
	     * Returns the start offset within the selected text.
	     * If there is no selection, but there is
	     * a caret, the start and end offsets will be the same.
	     *
	     * @return the index into the text of the start of the selection
	     */
	    public int getSelectionStart() {
		return editor.getSelectionStart();
	    }
	    
	    /**
	     * Returns the end offset within the selected text.
	     * If there is no selection, but there is
	     * a caret, the start and end offsets will be the same.
	     *
	     * @return the index into teh text of the end of the selection
	     */
	    public int getSelectionEnd() {
		return editor.getSelectionEnd();
	    }
	    
	    /**
	     * Returns the portion of the text that is selected. 
	     *
	     * @return the String portion of the text that is selected
	     */
	    public String getSelectedText() {
		return editor.getSelectedText();
	    }
	    
	    /*
	     * Returns the text substring starting at the specified
	     * offset with the specified length.
	     */
	    private String getText(int offset, int length) 
		throws BadLocationException {
		
		if (model != null && model instanceof StyledDocument) {
		    StyledDocument doc = (StyledDocument)model;
		    return model.getText(offset, length);
		} else {
		    return null;
		}
	    }
	}
    }
	
    /*
     * ElementInfo for images
     */
    private class IconElementInfo extends ElementInfo implements Accessible {

        private int width = -1;
        private int height = -1;

        IconElementInfo(Element element, ElementInfo parent) {
            super(element, parent);
        }

        protected void invalidate(boolean first) {
            super.invalidate(first);
            width = height = -1;
        }

	private int getImageSize(Object key) {
            if (validateIfNecessary()) {
                int size = getIntAttr(getAttributes(), key, -1);

                if (size == -1) {
                    View v = getView();

                    size = 0;
                    if (v instanceof ImageView) {
                        Image img = ((ImageView)v).getImage();
                        if (img != null) {
                            if (key == HTML.Attribute.WIDTH) {
                                size = img.getWidth(null);
                            }
                            else {
                                size = img.getHeight(null);
                            }
                        }
                    }
                }
                return size;
            }
            return 0;
	}

	// begin AccessibleIcon implementation ...
	private AccessibleContext accessibleContext;
	
	public AccessibleContext getAccessibleContext() {
	    if (accessibleContext == null) {
		accessibleContext = new IconAccessibleContext(this);
	    }
	    return accessibleContext;
	}
	
	/*
	 * AccessibleContext for images
	 */
	protected class IconAccessibleContext extends HTMLAccessibleContext
            implements AccessibleIcon  {

	    public IconAccessibleContext(ElementInfo elementInfo) {
		super(elementInfo);
	    }
	    
	    /**
	     * Gets the accessibleName property of this object.  The accessibleName
	     * property of an object is a localized String that designates the purpose
	     * of the object.  For example, the accessibleName property of a label
	     * or button might be the text of the label or button itself.  In the
	     * case of an object that doesn't display its name, the accessibleName
	     * should still be set.  For example, in the case of a text field used
	     * to enter the name of a city, the accessibleName for the en_US locale
	     * could be 'city.'
	     *
	     * @return the localized name of the object; null if this 
	     * object does not have a name
	     *
	     * @see #setAccessibleName
	     */
	    public String getAccessibleName() {
		return getAccessibleIconDescription();
	    }
	    
	    /**
	     * Gets the accessibleDescription property of this object.  If this
	     * property isn't set, returns the content type of this
	     * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text").
	     *
	     * @return the localized description of the object; <code>null</code>
	     * 	if this object does not have a description
	     *
	     * @see #setAccessibleName
	     */
	    public String getAccessibleDescription() {
		return editor.getContentType();
	    }
	    
	    /**
	     * Gets the role of this object.  The role of the object is the generic
	     * purpose or use of the class of this object.  For example, the role
	     * of a push button is AccessibleRole.PUSH_BUTTON.  The roles in 
	     * AccessibleRole are provided so component developers can pick from
	     * a set of predefined roles.  This enables assistive technologies to
	     * provide a consistent interface to various tweaked subclasses of 
	     * components (e.g., use AccessibleRole.PUSH_BUTTON for all components
	     * that act like a push button) as well as distinguish between sublasses
	     * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes
	     * and AccessibleRole.RADIO_BUTTON for radio buttons).
	     * <p>Note that the AccessibleRole class is also extensible, so 
	     * custom component developers can define their own AccessibleRole's
	     * if the set of predefined roles is inadequate.
	     *
	     * @return an instance of AccessibleRole describing the role of the object
	     * @see AccessibleRole
	     */
	    public AccessibleRole getAccessibleRole() {
		return AccessibleRole.ICON;
	    }
	    
	    public AccessibleIcon [] getAccessibleIcon() {
		AccessibleIcon [] icons = new AccessibleIcon[1];
		icons[0] = this;
		return icons;
	    }

	    /**
	     * Gets the description of the icon.  This is meant to be a brief
	     * textual description of the object.  For example, it might be
	     * presented to a blind user to give an indication of the purpose
	     * of the icon.
	     *
	     * @return the description of the icon
	     */
	    public String getAccessibleIconDescription() {
		return ((ImageView)getView()).getAltText();
	    }
	
	    /**
	     * Sets the description of the icon.  This is meant to be a brief
	     * textual description of the object.  For example, it might be
	     * presented to a blind user to give an indication of the purpose
	     * of the icon.
	     *
	     * @param description the description of the icon
	     */
	    public void setAccessibleIconDescription(String description) {
	    }
	
	    /**
	     * Gets the width of the icon
	     *
	     * @return the width of the icon.
	     */
	    public int getAccessibleIconWidth() {
		if (width == -1) {
		    width = getImageSize(HTML.Attribute.WIDTH);
		}
		return width;
	    }
	    
	    /**
	     * Gets the height of the icon
	     *
	     * @return the height of the icon.
	     */
	    public int getAccessibleIconHeight() {
		if (height == -1) {
		    height = getImageSize(HTML.Attribute.HEIGHT);
		}
		return height;
	    }
	}
	// ... end AccessibleIconImplementation
    }


    /**
     * TableElementInfo encapsulates information about a HTML.Tag.TABLE.
     * To make access fast it crates a grid containing the children to
     * allow for access by row, column. TableElementInfo will contain
     * TableRowElementInfos, which will contain TableCellElementInfos.
     * Any time one of the rows or columns becomes invalid the table is
     * invalidated.  This is because any time one of the child attributes
     * changes the size of the grid may have changed.
     */
    private class TableElementInfo extends ElementInfo 
        implements Accessible {

	protected ElementInfo caption;

        /**
         * Allocation of the table by row x column. There may be holes (eg 
         * nulls) depending upon the html, any cell that has a rowspan/colspan
         * > 1 will be contained multiple times in the grid.
         */
        private TableCellElementInfo[][] grid;


        TableElementInfo(Element e, ElementInfo parent) {
            super(e, parent);
        }

	public ElementInfo getCaptionInfo() {
	    return caption;
	}

        /**
         * Overriden to update the grid when validating.
         */
        protected void validate() {
            super.validate();
            updateGrid();
        }

        /**
         * Overriden to only alloc instances of TableRowElementInfos.
         */
        protected void loadChildren(Element e) {

            for (int counter = 0; counter < e.getElementCount(); counter++) {
                Element child = e.getElement(counter);
                AttributeSet attrs = child.getAttributes();

                if (attrs.getAttribute(StyleConstants.NameAttribute) ==
                                       HTML.Tag.TR) {
                    addChild(new TableRowElementInfo(child, this, counter));

                } else if (attrs.getAttribute(StyleConstants.NameAttribute) ==
                                       HTML.Tag.CAPTION) {
		    // Handle captions as a special case since all other
		    // children are table rows.
		    caption = createElementInfo(child, this);
		}
            }
        }

        /**
         * Updates the grid.
         */
        private void updateGrid() {
            // Determine the max row/col count.
            int delta = 0;
            int maxCols = 0;
            int rows = 0;
            for (int counter = 0; counter < getChildCount(); counter++) {
                TableRowElementInfo row = getRow(counter);
                int prev = 0;
                for (int y = 0; y < delta; y++) {
                    prev = Math.max(prev, getRow(counter - y - 1).
                                    getColumnCount(y + 2));
                }
                delta = Math.max(row.getRowCount(), delta);
                delta--;
                maxCols = Math.max(maxCols, row.getColumnCount() + prev);
            }
            rows = getChildCount() + delta;

            // Alloc
            grid = new TableCellElementInfo[rows][];
            for (int counter = 0; counter < rows; counter++) {
                grid[counter] = new TableCellElementInfo[maxCols];
            }
            // Update
            for (int counter = 0; counter < rows; counter++) {
                getRow(counter).updateGrid(counter);
            }
        }

        /**
         * Returns the TableCellElementInfo at the specified index.
         */
        public TableRowElementInfo getRow(int index) {
            return (TableRowElementInfo)getChild(index);
        }

        /**
         * Returns the TableCellElementInfo by row and column.
         */
        public TableCellElementInfo getCell(int r, int c) {
            if (validateIfNecessary() && r < grid.length &&
                                         c < grid[0].length) {
                return grid[r][c];
            }
            return null;
        }

        /**
         * Returns the rowspan of the specified entry.
         */
        public int getRowExtentAt(int r, int c) {
            TableCellElementInfo cell = getCell(r, c);

            if (cell != null) {
                int rows = cell.getRowCount();
                int delta = 1;

                while ((r - delta) >= 0 && grid[r - delta][c] == cell) {
                    delta++;
                }
                return rows - delta + 1;
            }
            return 0;
        }

        /**
         * Returns the colspan of the specified entry.
         */
        public int getColumnExtentAt(int r, int c) {
            TableCellElementInfo cell = getCell(r, c);

            if (cell != null) {
                int cols = cell.getColumnCount();
                int delta = 1;

                while ((c - delta) >= 0 && grid[r][c - delta] == cell) {
                    delta++;
                }
                return cols - delta + 1;
            }
            return 0;
        }

        /**
         * Returns the number of rows in the table.
         */
        public int getRowCount() {
            if (validateIfNecessary()) {
                return grid.length;
            }
            return 0;
        }

        /**
         * Returns the number of columns in the table.
         */
        public int getColumnCount() {
            if (validateIfNecessary() && grid.length > 0) {
                return grid[0].length;
            }
            return 0;
        }

	// begin AccessibleTable implementation ...
	private AccessibleContext accessibleContext;
	
	public AccessibleContext getAccessibleContext() {
	    if (accessibleContext == null) {
		accessibleContext = new TableAccessibleContext(this);
	    }
	    return accessibleContext;
	}
	
	/*
	 * AccessibleContext for tables
	 */
	public class TableAccessibleContext extends HTMLAccessibleContext
            implements AccessibleTable {

	    private AccessibleHeadersTable rowHeadersTable;

	    public TableAccessibleContext(ElementInfo elementInfo) {
		super(elementInfo);
	    }
	    
	    /**
	     * Gets the accessibleName property of this object.  The accessibleName
	     * property of an object is a localized String that designates the purpose
	     * of the object.  For example, the accessibleName property of a label
	     * or button might be the text of the label or button itself.  In the
	     * case of an object that doesn't display its name, the accessibleName
	     * should still be set.  For example, in the case of a text field used
	     * to enter the name of a city, the accessibleName for the en_US locale
	     * could be 'city.'
	     *
	     * @return the localized name of the object; null if this 
	     * object does not have a name
	     *
	     * @see #setAccessibleName
	     */
	    public String getAccessibleName() {
		// return the role of the object
		return getAccessibleRole().toString();
	    }
	    
	    /**
	     * Gets the accessibleDescription property of this object.  If this
	     * property isn't set, returns the content type of this
	     * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text").
	     *
	     * @return the localized description of the object; <code>null</code>
	     * 	if this object does not have a description
	     *
	     * @see #setAccessibleName
	     */
	    public String getAccessibleDescription() {
		return editor.getContentType();
	    }
	    
	    /**
	     * Gets the role of this object.  The role of the object is the generic
	     * purpose or use of the class of this object.  For example, the role
	     * of a push button is AccessibleRole.PUSH_BUTTON.  The roles in 
	     * AccessibleRole are provided so component developers can pick from
	     * a set of predefined roles.  This enables assistive technologies to
	     * provide a consistent interface to various tweaked subclasses of 
	     * components (e.g., use AccessibleRole.PUSH_BUTTON for all components
	     * that act like a push button) as well as distinguish between sublasses
	     * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes
	     * and AccessibleRole.RADIO_BUTTON for radio buttons).
	     * <p>Note that the AccessibleRole class is also extensible, so 
	     * custom component developers can define their own AccessibleRole's
	     * if the set of predefined roles is inadequate.
	     *
	     * @return an instance of AccessibleRole describing the role of the object
	     * @see AccessibleRole
	     */
	    public AccessibleRole getAccessibleRole() {
		return AccessibleRole.TABLE;
	    }
	    
	    /**
	     * Gets the 0-based index of this object in its accessible parent.
	     *
	     * @return the 0-based index of this object in its parent; -1 if this 
	     * object does not have an accessible parent.
	     *
	     * @see #getAccessibleParent 
	     * @see #getAccessibleChildrenCount
	     * @gsee #getAccessibleChild
	     */
	    public int getAccessibleIndexInParent() {
		return elementInfo.getIndexInParent();
	    }
	    
	    /**
	     * Returns the number of accessible children of the object.
	     *
	     * @return the number of accessible children of the object.
	     */
	    public int getAccessibleChildrenCount() {
		return ((TableElementInfo)elementInfo).getRowCount() * 
		    ((TableElementInfo)elementInfo).getColumnCount();
	    }
	    
	    /**
	     * Returns the specified Accessible child of the object.  The Accessible
	     * children of an Accessible object are zero-based, so the first child 
	     * of an Accessible child is at index 0, the second child is at index 1,
	     * and so on.
	     *
	     * @param i zero-based index of child
	     * @return the Accessible child of the object
	     * @see #getAccessibleChildrenCount
	     */
	    public Accessible getAccessibleChild(int i) {
		int rowCount = ((TableElementInfo)elementInfo).getRowCount();
		int columnCount = ((TableElementInfo)elementInfo).getColumnCount();
		int r = i / rowCount;
		int c = i % columnCount;
		if (r < 0 || r >= rowCount || c < 0 || c >= columnCount) {
		    return null;
		} else {
		    return getAccessibleAt(r, c);
		}
	    }
	    
	    public AccessibleTable getAccessibleTable() {
		return this;
	    }
	    
	    /**
	     * Returns the caption for the table.
	     *
	     * @return the caption for the table
	     */
	    public Accessible getAccessibleCaption() {
		ElementInfo captionInfo = getCaptionInfo();
		if (captionInfo instanceof Accessible) {
		    return (Accessible)caption;
		} else {
		    return null;
		}
	    }
	    
	    /**
	     * Sets the caption for the table.
	     *
	     * @param a the caption for the table
	     */
	    public void setAccessibleCaption(Accessible a) {
	    }
	    
	    /**
	     * Returns the summary description of the table.
	     * 
	     * @return the summary description of the table
	     */
	    public Accessible getAccessibleSummary() {
		return null;	
	    }
	    
	    /**
	     * Sets the summary description of the table
	     *
	     * @param a the summary description of the table
	     */
	    public void setAccessibleSummary(Accessible a) {
	    }
	    
	    /**
	     * Returns the number of rows in the table.
	     *
	     * @return the number of rows in the table
	     */
	    public int getAccessibleRowCount() {
		return ((TableElementInfo)elementInfo).getRowCount();
	    }
	    
	    /**
	     * Returns the number of columns in the table.
	     *
	     * @return the number of columns in the table
	     */
	    public int getAccessibleColumnCount() {
		return ((TableElementInfo)elementInfo).getColumnCount();
	    }
	    
	    /**
	     * Returns the Accessible at a specified row and column
	     * in the table.
	     *
	     * @param r zero-based row of the table
	     * @param c zero-based column of the table
	     * @return the Accessible at the specified row and column
	     */
	    public Accessible getAccessibleAt(int r, int c) {
		TableCellElementInfo cellInfo = getCell(r, c);
		if (cellInfo != null) {
		    return cellInfo.getAccessible();
		} else {
		    return null;
		}
	    }
	    
	    /**
	     * Returns the number of rows occupied by the Accessible at
	     * a specified row and column in the table.
	     *
	     * @return the number of rows occupied by the Accessible at a
	     * given specified (row, column)
	     */
	    public int getAccessibleRowExtentAt(int r, int c) {
		return ((TableElementInfo)elementInfo).getRowExtentAt(r, c);
	    }
	    
	    /**
	     * Returns the number of columns occupied by the Accessible at
	     * a specified row and column in the table.
	     *
	     * @return the number of columns occupied by the Accessible at a
	     * given specified row and column
	     */
	    public int getAccessibleColumnExtentAt(int r, int c) {
		return ((TableElementInfo)elementInfo).getColumnExtentAt(r, c);
	    }
	    
	    /**
	     * Returns the row headers as an AccessibleTable.
	     *
	     * @return an AccessibleTable representing the row
	     * headers
	     */
	    public AccessibleTable getAccessibleRowHeader() {
		return rowHeadersTable;
	    }
	    
	    /**
	     * Sets the row headers.
	     *
	     * @param table an AccessibleTable representing the
	     * row headers
	     */
	    public void setAccessibleRowHeader(AccessibleTable table) {
	    }
	    
	    /**
	     * Returns the column headers as an AccessibleTable.
	     *
	     * @return an AccessibleTable representing the column
	     * headers
	     */
	    public AccessibleTable getAccessibleColumnHeader() {
		return null; 
	    }
	    
	    /**
	     * Sets the column headers.
	     *
	     * @param table an AccessibleTable representing the
	     * column headers
	     */
	    public void setAccessibleColumnHeader(AccessibleTable table) {
	    }
	    
	    /**
	     * Returns the description of the specified row in the table.
	     *
	     * @param r zero-based row of the table
	     * @return the description of the row
	     */
	    public Accessible getAccessibleRowDescription(int r) {
		return null;
	    }
	    
	    /**
	     * Sets the description text of the specified row of the table.
	     *
	     * @param r zero-based row of the table
	     * @param a the description of the row
	     */
	    public void setAccessibleRowDescription(int r, Accessible a) {
	    }
	    
	    /**
	     * Returns the description text of the specified column in the table.
	     *
	     * @param c zero-based column of the table
	     * @return the text description of the column
	     */
	    public Accessible getAccessibleColumnDescription(int c) {
		return null;
	    }
	    
	    /**
	     * Sets the description text of the specified column in the table.
	     *
	     * @param c zero-based column of the table
	     * @param a the text description of the column
	     */
	    public void setAccessibleColumnDescription(int c, Accessible a) {
	    }
	    
	    /**
	     * Returns a boolean value indicating whether the accessible at
	     * a specified row and column is selected.
	     *
	     * @param r zero-based row of the table
	     * @param c zero-based column of the table
	     * @return the boolean value true if the accessible at the
	     * row and column is selected. Otherwise, the boolean value 
	     * false
	     */
	    public boolean isAccessibleSelected(int r, int c) {
		if (validateIfNecessary()) {
		    if (r < 0 || r >= getAccessibleRowCount() ||
			c < 0 || c >= getAccessibleColumnCount()) {
			return false;
		    }
		    TableCellElementInfo cell = getCell(r, c);
		    if (cell != null) {
			Element elem = cell.getElement();
			int start = elem.getStartOffset();
			int end = elem.getEndOffset();
			return start >= editor.getSelectionStart() &&
			    end <= editor.getSelectionEnd();
		    }
		}
		return false;
	    }
	    
	    /**
	     * Returns a boolean value indicating whether the specified row
	     * is selected.
	     *
	     * @param r zero-based row of the table
	     * @return the boolean value true if the specified row is selected.
	     * Otherwise, false.
	     */
	    public boolean isAccessibleRowSelected(int r) {
		if (validateIfNecessary()) {
		    if (r < 0 || r >= getAccessibleRowCount()) {
			return false;
		    }
		    int nColumns = getAccessibleColumnCount();

		    TableCellElementInfo startCell = getCell(r, 0);
		    if (startCell == null) {
			return false;
		    }
		    int start = startCell.getElement().getStartOffset();

		    TableCellElementInfo endCell = getCell(r, nColumns-1);
		    if (endCell == null) {
			return false;
		    }
		    int end = endCell.getElement().getEndOffset();

		    return start >= editor.getSelectionStart() &&
			end <= editor.getSelectionEnd();
		}
		return false;
	    }
	    
	    /**
	     * Returns a boolean value indicating whether the specified column
	     * is selected.
	     *
	     * @param r zero-based column of the table
	     * @return the boolean value true if the specified column is selected.
	     * Otherwise, false.
	     */
	    public boolean isAccessibleColumnSelected(int c) {
		if (validateIfNecessary()) {
		    if (c < 0 || c >= getAccessibleColumnCount()) {
			return false;
		    }
		    int nRows = getAccessibleRowCount();

		    TableCellElementInfo startCell = getCell(0, c);
		    if (startCell == null) {
			return false;
		    }
		    int start = startCell.getElement().getStartOffset();

		    TableCellElementInfo endCell = getCell(nRows-1, c);
		    if (endCell == null) {
			return false;
		    }
		    int end = endCell.getElement().getEndOffset();
		    return start >= editor.getSelectionStart() &&
			end <= editor.getSelectionEnd();
		}
		return false;
	    }
	    
	    /**
	     * Returns the selected rows in a table.
	     *
	     * @return an array of selected rows where each element is a
	     * zero-based row of the table
	     */
	    public int [] getSelectedAccessibleRows() {
		if (validateIfNecessary()) {
		    int nRows = getAccessibleRowCount();
		    Vector vec = new Vector();
		    
		    for (int i = 0; i < nRows; i++) {
			if (isAccessibleRowSelected(i)) {
			    vec.addElement(new Integer(i));
			}
		    }
		    int retval[] = new int[vec.size()];
		    for (int i = 0; i < retval.length; i++) {
			retval[i] = ((Integer)vec.elementAt(i)).intValue();
		    }
		    return retval;
		}
		return new int[0];
	    }
	    
	    /**
	     * Returns the selected columns in a table.
	     *
	     * @return an array of selected columns where each element is a
	     * zero-based column of the table
	     */
	    public int [] getSelectedAccessibleColumns() {
		if (validateIfNecessary()) {
		    int nColumns = getAccessibleRowCount();
		    Vector vec = new Vector();
		    
		    for (int i = 0; i < nColumns; i++) {
			if (isAccessibleColumnSelected(i)) {
			    vec.addElement(new Integer(i));
			}
		    }
		    int retval[] = new int[vec.size()];
		    for (int i = 0; i < retval.length; i++) {
			retval[i] = ((Integer)vec.elementAt(i)).intValue();
		    }
		    return retval;
		}
		return new int[0];
	    }
	    
	    // begin AccessibleExtendedTable implementation -------------
	    
	    /**
	     * Returns the row number of an index in the table.
	     *
	     * @param index the zero-based index in the table
	     * @return the zero-based row of the table if one exists;
	     * otherwise -1.
	     */
	    public int getAccessibleRow(int index) {
		if (validateIfNecessary()) {
		    int numCells = getAccessibleColumnCount() * 
			getAccessibleRowCount();
		    if (index >= numCells) {
			return -1;
		    } else {
			return index / getAccessibleColumnCount();
		    }
		}
		return -1;
	    }
	    
	    /**
	     * Returns the column number of an index in the table.
	     *
	     * @param index the zero-based index in the table
	     * @return the zero-based column of the table if one exists;
	     * otherwise -1.
	     */
	    public int getAccessibleColumn(int index) {
		if (validateIfNecessary()) {
		    int numCells = getAccessibleColumnCount() * 
			getAccessibleRowCount();
		    if (index >= numCells) {
			return -1;
		    } else {
			return index % getAccessibleColumnCount();
		    }
		}
		return -1;
	    }
	    
	    /**
	     * Returns the index at a row and column in the table.
	     *
	     * @param r zero-based row of the table
	     * @param c zero-based column of the table
	     * @return the zero-based index in the table if one exists;
	     * otherwise -1.
	     */
	    public int getAccessibleIndex(int r, int c) {
		if (validateIfNecessary()) {
		    if (r >= getAccessibleRowCount() ||
			c >= getAccessibleColumnCount()) {
			return -1;
		    } else {
			return r * getAccessibleColumnCount() + c;
		    }
		}
		return -1;
	    }
	    
	    /**
	     * Returns the row header at a row in a table.
	     * @param r zero-based row of the table
	     *
	     * @return a String representing the row header
	     * if one exists; otherwise null.
	     */
	    public String getAccessibleRowHeader(int r) {
		if (validateIfNecessary()) {
		    TableCellElementInfo cellInfo = getCell(r, 0);
		    if (cellInfo.isHeaderCell()) {
			View v = cellInfo.getView();
			if (v != null && model != null) {
			    try {
				return model.getText(v.getStartOffset(),
						     v.getEndOffset() -
						     v.getStartOffset());
			    } catch (BadLocationException e) {
				return null;
			    }
			}
		    }
		}
		return null;
	    }
	    
	    /**
	     * Returns the column header at a column in a table.
	     * @param c zero-based column of the table
	     *
	     * @return a String representing the column header
	     * if one exists; otherwise null.
	     */
	    public String getAccessibleColumnHeader(int c) {
		if (validateIfNecessary()) {
		    TableCellElementInfo cellInfo = getCell(0, c);
		    if (cellInfo.isHeaderCell()) {
			View v = cellInfo.getView();
			if (v != null && model != null) {
			    try {
				return model.getText(v.getStartOffset(),
						     v.getEndOffset() -
						     v.getStartOffset());
			    } catch (BadLocationException e) {
				return null;
			    }
			}
		    }
		}
		return null;
	    }

	    public void addRowHeader(TableCellElementInfo cellInfo, int rowNumber) {
		if (rowHeadersTable == null) {
		    rowHeadersTable = new AccessibleHeadersTable();
		}
		rowHeadersTable.addHeader(cellInfo, rowNumber);
	    }
	    // end of AccessibleExtendedTable implementation ------------

	    protected class AccessibleHeadersTable implements AccessibleTable {
            
		// Header information is modeled as a Hashtable of
		// ArrayLists where each Hashtable entry represents
		// a row containing one or more headers.
		private Hashtable headers = new Hashtable();
		private int rowCount = 0;
		private int columnCount = 0;

		public void addHeader(TableCellElementInfo cellInfo, int rowNumber) {
		    Integer rowInteger = new Integer(rowNumber);
		    ArrayList list = (ArrayList)headers.get(rowInteger);
		    if (list == null) {
			list = new ArrayList();
			headers.put(rowInteger, list);
		    }
		    list.add(cellInfo);
		}

		/**
		 * Returns the caption for the table.
		 *
		 * @return the caption for the table
		 */
		public Accessible getAccessibleCaption() {
		    return null;
		}
		
		/**
		 * Sets the caption for the table.
		 *
		 * @param a the caption for the table
		 */
		public void setAccessibleCaption(Accessible a) {
		}
		
		/**
		 * Returns the summary description of the table.
		 * 
		 * @return the summary description of the table
		 */
		public Accessible getAccessibleSummary() {
		    return null;
		}
		
		/**
		 * Sets the summary description of the table
		 *
		 * @param a the summary description of the table
		 */
		public void setAccessibleSummary(Accessible a) {
		}
		
		/**
		 * Returns the number of rows in the table.
		 *
		 * @return the number of rows in the table
		 */
		public int getAccessibleRowCount() {
		    return rowCount;
		}
		
		/**
		 * Returns the number of columns in the table.
		 *
		 * @return the number of columns in the table
		 */
		public int getAccessibleColumnCount() {
		    return columnCount;
		}
		
		private TableCellElementInfo getElementInfoAt(int r, int c) {
		    ArrayList list = (ArrayList)headers.get(new Integer(r));
		    if (list != null) {
			return (TableCellElementInfo)list.get(c);
		    } else {
			return null;
		    }
		}

		/**
		 * Returns the Accessible at a specified row and column
		 * in the table.
		 *
		 * @param r zero-based row of the table
		 * @param c zero-based column of the table
		 * @return the Accessible at the specified row and column
		 */
		public Accessible getAccessibleAt(int r, int c) {
		    ElementInfo elementInfo = getElementInfoAt(r, c);
		    if (elementInfo instanceof Accessible) {
			return (Accessible)elementInfo;
		    } else {
			return null;
		    }
		}
		
		/**
		 * Returns the number of rows occupied by the Accessible at
 		 * a specified row and column in the table.
		 *
		 * @return the number of rows occupied by the Accessible at a
		 * given specified (row, column)
		 */
		public int getAccessibleRowExtentAt(int r, int c) {
		    TableCellElementInfo elementInfo = getElementInfoAt(r, c);
		    if (elementInfo != null) {
			return elementInfo.getRowCount();
		    } else {
			return 0;
		    }
		}
		
		/**
		 * Returns the number of columns occupied by the Accessible at
		 * a specified row and column in the table.
		 *
		 * @return the number of columns occupied by the Accessible at a
		 * given specified row and column
		 */
		public int getAccessibleColumnExtentAt(int r, int c) {
		    TableCellElementInfo elementInfo = getElementInfoAt(r, c);
		    if (elementInfo != null) {
			return elementInfo.getRowCount();
		    } else {
			return 0;
		    }
		}
		
		/**
		 * Returns the row headers as an AccessibleTable.
		 *
		 * @return an AccessibleTable representing the row
		 * headers
		 */
		public AccessibleTable getAccessibleRowHeader() {
		    return null;
		}
		
		/**
		 * Sets the row headers.
		 *
		 * @param table an AccessibleTable representing the
		 * row headers
		 */
		public void setAccessibleRowHeader(AccessibleTable table) {
		}
		
		/**
		 * Returns the column headers as an AccessibleTable.
		 *
		 * @return an AccessibleTable representing the column
		 * headers
		 */
		public AccessibleTable getAccessibleColumnHeader() {
		    return null;
		}
		
		/**
		 * Sets the column headers.
		 *
		 * @param table an AccessibleTable representing the
		 * column headers
		 */
		public void setAccessibleColumnHeader(AccessibleTable table) {
		}
		
		/**
		 * Returns the description of the specified row in the table.
		 *
		 * @param r zero-based row of the table
		 * @return the description of the row
		 */
		public Accessible getAccessibleRowDescription(int r) {
		    return null;
		}
		
		/**
		 * Sets the description text of the specified row of the table.
		 *
		 * @param r zero-based row of the table
		 * @param a the description of the row
		 */
		public void setAccessibleRowDescription(int r, Accessible a) {
		}
		
		/**
		 * Returns the description text of the specified column in the table.
		 *
		 * @param c zero-based column of the table
		 * @return the text description of the column
		 */
		public Accessible getAccessibleColumnDescription(int c) {
		    return null;
		}
		
		/**
		 * Sets the description text of the specified column in the table.
		 *
		 * @param c zero-based column of the table
		 * @param a the text description of the column
		 */
		public void setAccessibleColumnDescription(int c, Accessible a) {
		}
		
		/**
		 * Returns a boolean value indicating whether the accessible at
		 * a specified row and column is selected.
		 *
		 * @param r zero-based row of the table
		 * @param c zero-based column of the table
		 * @return the boolean value true if the accessible at the
		 * row and column is selected. Otherwise, the boolean value 
		 * false
		 */
		public boolean isAccessibleSelected(int r, int c) {
		    return false;
		}
		
		/**
		 * Returns a boolean value indicating whether the specified row
		 * is selected.
		 *
		 * @param r zero-based row of the table
		 * @return the boolean value true if the specified row is selected.
		 * Otherwise, false.
		 */
		public boolean isAccessibleRowSelected(int r) {
		    return false;
		}
		
		/**
		 * Returns a boolean value indicating whether the specified column
		 * is selected.
		 *
		 * @param r zero-based column of the table
		 * @return the boolean value true if the specified column is selected.
		 * Otherwise, false.
		 */
		public boolean isAccessibleColumnSelected(int c) {
		    return false;
		}
		
		/**
		 * Returns the selected rows in a table.
		 *
		 * @return an array of selected rows where each element is a
		 * zero-based row of the table
		 */
		public int [] getSelectedAccessibleRows() {
		    return new int [0];
		}
		
		/**
		 * Returns the selected columns in a table.
		 *
		 * @return an array of selected columns where each element is a
		 * zero-based column of the table
		 */
		public int [] getSelectedAccessibleColumns() {
		    return new int [0];
		}
	    }
        } // ... end AccessibleHeadersTable
	
	/*
	 * ElementInfo for table rows
	 */
        private class TableRowElementInfo extends ElementInfo {

	    private TableElementInfo parent;
	    private int rowNumber;

            TableRowElementInfo(Element e, TableElementInfo parent, int rowNumber) {
                super(e, parent);
		this.parent = parent;
		this.rowNumber = rowNumber;
            }

            protected void loadChildren(Element e) {
                for (int x = 0; x < e.getElementCount(); x++) {
                    AttributeSet attrs = e.getElement(x).getAttributes();

                    if (attrs.getAttribute(StyleConstants.NameAttribute) == 
			    HTML.Tag.TH) {
			TableCellElementInfo headerElementInfo = 
			    new TableCellElementInfo(e.getElement(x), this, true);
			addChild(headerElementInfo);

			AccessibleTable at = 
			    parent.getAccessibleContext().getAccessibleTable();
			TableAccessibleContext tableElement = 
			    (TableAccessibleContext)at;
			tableElement.addRowHeader(headerElementInfo, rowNumber);

		    } else if (attrs.getAttribute(StyleConstants.NameAttribute) == 
			    HTML.Tag.TD) {
                        addChild(new TableCellElementInfo(e.getElement(x), this, 
							  false));
                    }
                }
            }

            /**
             * Returns the max of the rowspans of the cells in this row.
             */
            public int getRowCount() {
                int rowCount = 1;
                if (validateIfNecessary()) {
                    for (int counter = 0; counter < getChildCount(); 
			 counter++) {

                        TableCellElementInfo cell = (TableCellElementInfo)
                                                    getChild(counter);

                        if (cell.validateIfNecessary()) {
			    rowCount = Math.max(rowCount, cell.getRowCount());
                        }
                    }
                }
                return rowCount;
            }

            /**
             * Returns the sum of the column spans of the individual
             * cells in this row.
             */
            public int getColumnCount() {
                int colCount = 0;
                if (validateIfNecessary()) {
                    for (int counter = 0; counter < getChildCount();
                         counter++) {
                        TableCellElementInfo cell = (TableCellElementInfo)
                                                    getChild(counter);

                        if (cell.validateIfNecessary()) {
			    colCount += cell.getColumnCount();
                        }
                    }
                }
                return colCount;
            }

            /**
             * Overriden to invalidate the table as well as
             * TableRowElementInfo.
             */
            protected void invalidate(boolean first) {
                super.invalidate(first);
                getParent().invalidate(true);
            }

            /**
             * Places the TableCellElementInfos for this element in
             * the grid.
             */
            private void updateGrid(int row) {
                if (validateIfNecessary()) {
                    boolean emptyRow = false;

                    while (!emptyRow) {
                        for (int counter = 0; counter < grid[row].length;
                                 counter++) {
                            if (grid[row][counter] == null) {
                                emptyRow = true;
                                break;
                            }
                        }
                        if (!emptyRow) {
                            row++;
                        }
                    }
                    for (int col = 0, counter = 0; counter < getChildCount();
                             counter++) {
                        TableCellElementInfo cell = (TableCellElementInfo)
                                                    getChild(counter);

                        while (grid[row][col] != null) {
                            col++;
                        }
                        for (int rowCount = cell.getRowCount() - 1;
                             rowCount >= 0; rowCount--) {
                            for (int colCount = cell.getColumnCount() - 1;
                                 colCount >= 0; colCount--) {
                                grid[row + rowCount][col + colCount] = cell;
                            }
                        }
                        col += cell.getColumnCount();
                    }
                }
            }

            /**
             * Returns the column count of the number of columns that have
             * a rowcount >= rowspan.
             */
            private int getColumnCount(int rowspan) {
                if (validateIfNecessary()) {
                    int cols = 0;
                    for (int counter = 0; counter < getChildCount();
                         counter++) {
                        TableCellElementInfo cell = (TableCellElementInfo)
                                                    getChild(counter);

                        if (cell.getRowCount() >= rowspan) {
                            cols += cell.getColumnCount();
                        }
                    }
                    return cols;
                }
                return 0;
            }
	}

        /**
         * TableCellElementInfo is used to represents the cells of
         * the table.
         */
        private class TableCellElementInfo extends ElementInfo {

	    private Accessible accessible;
	    private boolean isHeaderCell;

            TableCellElementInfo(Element e, ElementInfo parent) {
                super(e, parent);
		this.isHeaderCell = false;
            }

            TableCellElementInfo(Element e, ElementInfo parent, 
				 boolean isHeaderCell) {
                super(e, parent);
		this.isHeaderCell = isHeaderCell;
            }

	    /*
	     * Returns whether this table cell is a header
	     */
	    public boolean isHeaderCell() {
		return this.isHeaderCell;
	    }

	    /*
	     * Returns the Accessible representing this table cell
	     */
	    public Accessible getAccessible() {
		accessible = null;
		getAccessible(this);
		return accessible;
	    }

	    /*
	     * Gets the outermost Accessible in the table cell
	     */
	    private void getAccessible(ElementInfo elementInfo) {
		if (elementInfo instanceof Accessible) {
		    accessible = (Accessible)elementInfo;
		    return;
		} else {
		    for (int i = 0; i < elementInfo.getChildCount(); i++) {
			getAccessible(elementInfo.getChild(i));
		    }
		}
	    }     

            /**
             * Returns the rowspan attribute.
             */
            public int getRowCount() {
                if (validateIfNecessary()) {
                    return Math.max(1, getIntAttr(getAttributes(),
                                                  HTML.Attribute.ROWSPAN, 1));
                }
                return 0;
            }

            /**
             * Returns the colspan attribute.
             */
            public int getColumnCount() {
                if (validateIfNecessary()) {
                    return Math.max(1, getIntAttr(getAttributes(),
                                                  HTML.Attribute.COLSPAN, 1));
                }
                return 0;
            }

            /**
             * Overriden to invalidate the TableRowElementInfo as well as
             * the TableCellElementInfo.
             */
            protected void invalidate(boolean first) {
                super.invalidate(first);
                getParent().invalidate(true);
            }
	}
    }


    /**
     * ElementInfo provides a slim down view of an Element.  Each ElementInfo
     * can have any number of child ElementInfos that are not necessarily
     * direct children of the Element. As the Document changes various 
     * ElementInfos become invalidated. Before accessing a particular portion
     * of an ElementInfo you should make sure it is valid by invoking
     * <code>validateIfNecessary</code>, this will return true if
     * successful, on the other hand a false return value indicates the
     * ElementInfo is not valid and can never become valid again (usually
     * the result of the Element the ElementInfo encapsulates being removed).
     */
    private class ElementInfo {

        /**
         * The children of this ElementInfo.
         */
        private ArrayList children;
        /**
         * The Element this ElementInfo is providing information for.
         */
        private Element element;
        /**
         * The parent ElementInfo, will be null for the root.
         */
        private ElementInfo parent;
        /**
         * Indicates the validity of the ElementInfo.
         */
        private boolean isValid;
        /**
         * Indicates if the ElementInfo can become valid.
         */
        private boolean canBeValid;


        /**
         * Creates the root ElementInfo.
         */
        ElementInfo(Element element) {
            this(element, null);
        }

        /**
         * Creates an ElementInfo representing <code>element</code> with
         * the specified parent.
         */
        ElementInfo(Element element, ElementInfo parent) {
            this.element = element;
            this.parent = parent;
            isValid = false;
            canBeValid = true;
        }

        /**
         * Validates the receiver. This recreates the children as well. This
         * will be invoked within a <code>readLock</code>. If this is overriden
         * it MUST invoke supers implementation first!
         */
        protected void validate() {
            isValid = true;
            loadChildren(getElement());
        }

        /**
         * Recreates the direct children of <code>info</code>.
         */
        protected void loadChildren(Element parent) {
            if (!parent.isLeaf()) {
                for (int counter = 0, maxCounter = parent.getElementCount();
                    counter < maxCounter; counter++) {
                    Element e = parent.getElement(counter);
                    ElementInfo childInfo = createElementInfo(e, this);

                    if (childInfo != null) {
                        addChild(childInfo);
                    }
                    else {
                        loadChildren(e);
                    }
                }
            }
        }

        /**
         * Returns the index of the child in the parent, or -1 for the
         * root or if the parent isn't valid.
         */
        public int getIndexInParent() {
            if (parent == null || !parent.isValid()) {
                return -1;
            }
            return parent.indexOf(this);
        }

        /**
         * Returns the Element this <code>ElementInfo</code> represents.
         */
        public Element getElement() {
            return element;
        }

        /**
         * Returns the parent of this Element, or null for the root.
         */
        public ElementInfo getParent() {
            return parent;
        }

        /**
         * Returns the index of the specified child, or -1 if
         * <code>child</code> isn't a valid child.
         */
        public int indexOf(ElementInfo child) {
            ArrayList children = this.children;

            if (children != null) {
                return children.indexOf(child);
            }
            return -1;
        }

        /**
         * Returns the child ElementInfo at <code>index</code>, or null
         * if <code>index</code> isn't a valid index.
         */
        public ElementInfo getChild(int index) {
            if (validateIfNecessary()) {
                ArrayList children = this.children;

                if (children != null && index >= 0 &&
                                        index < children.size()) {
                    return (ElementInfo)children.get(index);
                }
            }
            return null;
        }

        /**
         * Returns the number of children the ElementInfo contains.
         */
        public int getChildCount() {
            validateIfNecessary();
            return (children == null) ? 0 : children.size();
        }

        /**
         * Adds a new child to this ElementInfo.
         */
        protected void addChild(ElementInfo child) {
            if (children == null) {
                children = new ArrayList();
            }
            children.add(child);
        }

        /**
         * Returns the View corresponding to this ElementInfo, or null
         * if the ElementInfo can't be validated.
         */
        protected View getView() {
            if (!validateIfNecessary()) {
                return null;
            }
            Object lock = lock();
            try {
                View rootView = getRootView();
                Element e = getElement();
                int start = e.getStartOffset();

                if (rootView != null) {
                    return getView(rootView, e, start);
                }
                return null;
            } finally {
                unlock(lock);
            }
        }

        /**
         * Returns the Bounds for this ElementInfo, or null
         * if the ElementInfo can't be validated.
         */
	public Rectangle getBounds() {
            if (!validateIfNecessary()) {
                return null;
            }
            Object lock = lock();
            try {
                Rectangle bounds = getRootEditorRect();
                View rootView = getRootView();
                Element e = getElement();

                if (bounds != null && rootView != null) {
                    try {
                        return rootView.modelToView(e.getStartOffset(),
                                                    Position.Bias.Forward,
                                                    e.getEndOffset(),
                                                    Position.Bias.Backward,
                                                    bounds).getBounds();
                    } catch (BadLocationException ble) { }
                }
            } finally {
                unlock(lock);
            }
            return null;
	}

        /**
         * Returns true if this ElementInfo is valid.
         */
        protected boolean isValid() {
            return isValid;
        }

        /**
         * Returns the AttributeSet associated with the Element, this will
         * return null if the ElementInfo can't be validated.
         */
        protected AttributeSet getAttributes() {
            if (validateIfNecessary()) {
                return getElement().getAttributes();
            }
            return null;
        }

        /**
         * Returns the AttributeSet associated with the View that is
         * representing this Element, this will
         * return null if the ElementInfo can't be validated.
         */
        protected AttributeSet getViewAttributes() {
            if (validateIfNecessary()) {
                View view = getView();

                if (view != null) {
                    return view.getElement().getAttributes();
                }
                return getElement().getAttributes();
            }
            return null;
        }

        /**
         * Convenience method for getting an integer attribute from the passed
         * in AttributeSet.
         */
        protected int getIntAttr(AttributeSet attrs, Object key, int deflt) {
            if (attrs != null && attrs.isDefined(key)) {
                int i;
                String val = (String)attrs.getAttribute(key);
                if (val == null) {
                    i = deflt;
                }
                else {
                    try {
                        i = Math.max(0, Integer.parseInt(val));
                    } catch (NumberFormatException x) {
                        i = deflt;
                    }
                }
                return i;
            }
            return deflt;
        }

        /**
         * Validates the ElementInfo if necessary.  Some ElementInfos may
         * never be valid again.  You should check <code>isValid</code> before
         * using one.  This will reload the children and invoke
         * <code>validate</code> if the ElementInfo is invalid and can become
         * valid again. This will return true if the receiver is valid.
         */
        protected boolean validateIfNecessary() {
            if (!isValid() && canBeValid) {
                children = null;
                Object lock = lock();

                try {
                    validate();
                } finally {
                    unlock(lock);
                }
            }
            return isValid();
        }

        /**
         * Invalidates the ElementInfo. Subclasses should override this
         * if they need to reset state once invalid.
         */
        protected void invalidate(boolean first) {
            if (!isValid()) {
                if (canBeValid && !first) {
                    canBeValid = false;
                }
                return;
            }
            isValid = false;
            canBeValid = first;
            if (children != null) {
                for (int counter = 0; counter < children.size(); counter++) {
                    ((ElementInfo)children.get(counter)).invalidate(false);
                }
                children = null;
            }
        }

        private View getView(View parent, Element e, int start) {
            if (parent.getElement() == e) {
                return parent;
            }
            int index = parent.getViewIndex(start, Position.Bias.Forward);

            if (index != -1 && index < parent.getViewCount()) {
                return getView(parent.getView(index), e, start);
            }
            return null;
        }

        private int getClosestInfoIndex(int index) {
            for (int counter = 0; counter < getChildCount(); counter++) {
                ElementInfo info = getChild(counter);

                if (index < info.getElement().getEndOffset() ||
                    index == info.getElement().getStartOffset()) {
                    return counter;
                }
            }
            return -1;
        }

        private void update(DocumentEvent e) {
            if (!isValid()) {
                return;
            }
            ElementInfo parent = getParent();
            Element element = getElement();

            do {
                DocumentEvent.ElementChange ec = e.getChange(element);
                if (ec != null) {
                    if (element == getElement()) {
                        // One of our children changed.
                        invalidate(true);
                    }
                    else if (parent != null) {
                        parent.invalidate(parent == getRootInfo());
                    }
                    return;
                }
                element = element.getParentElement();
            } while (parent != null && element != null &&
                     element != parent.getElement());

            if (getChildCount() > 0) {
                Element elem = getElement();
                int pos = e.getOffset();
                int index0 = getClosestInfoIndex(pos);
                if (index0 == -1 &&
                    e.getType() == DocumentEvent.EventType.REMOVE &&
                    pos >= elem.getEndOffset()) {
                    // Event beyond our offsets. We may have represented this,
                    // that is the remove may have removed one of our child
                    // Elements that represented this, so, we should foward
                    // to last element.
                    index0 = getChildCount() - 1;
                }
                ElementInfo info = (index0 >= 0) ? getChild(index0) : null;
                if (info != null &&
                    (info.getElement().getStartOffset() == pos) && (pos > 0)) {
                    // If at a boundary, forward the event to the previous
                    // ElementInfo too.
                    index0 = Math.max(index0 - 1, 0);
                }
                int index1;
                if (e.getType() != DocumentEvent.EventType.REMOVE) {
                    index1 = getClosestInfoIndex(pos + e.getLength());
                    if (index1 < 0) {
                        index1 = getChildCount() - 1;
                    }
                }
                else {
                    index1 = index0;
                    // A remove may result in empty elements.
                    while ((index1 + 1) < getChildCount() &&
                           getChild(index1 + 1).getElement().getEndOffset() ==
                           getChild(index1 + 1).getElement().getStartOffset()){
                        index1++;
                    }
                }
                index0 = Math.max(index0, 0);
                // The check for isValid is here as in the process of
                // forwarding update our child may invalidate us.
                for (int i = index0; i <= index1 && isValid(); i++) {
                    getChild(i).update(e);
                }
            }
        }
    }

    /**
     * DocumentListener installed on the current Document.  Will invoke
     * <code>update</code> on the <code>RootInfo</code> in response to
     * any event.
     */
    private class DocumentHandler implements DocumentListener {
        public void insertUpdate(DocumentEvent e) {
            getRootInfo().update(e);
        }
        public void removeUpdate(DocumentEvent e) {
            getRootInfo().update(e);
        }
        public void changedUpdate(DocumentEvent e) {
            getRootInfo().update(e);
        }
    }

    /*
     * PropertyChangeListener installed on the editor. 
     */
    private class PropertyChangeHandler implements PropertyChangeListener {
	public void propertyChange(PropertyChangeEvent evt) {
	    if (evt.getPropertyName().equals("document")) {
		// handle the document change
		setDocument(editor.getDocument());
	    }
	}
    }
}
